RESTful API Versioning
关于RESTful API Versioning,有很多讨论 Nobody Understands REST or HTTP, Versioning REST Services, best-practices-for-api-versioning, How are REST APIs versioned?, 总结如下。
为什么需要Versioning
尽管设计API的时候,我们尽可能设计完美的API,尽可能的避免修改API。但是随着业务需求的变更,API接口的变化几乎是无法避免的。
当业务需求变更的时候,可以这样选择:
- 保持接口的兼容性。这是一种方式,但是并非能切实做到的,为了兼容必定会损失一些新特性,或者牺牲良好的代码为代价。
- 修改API接口的同时,修改客户端。除非自己维护少量的客户端,否则这几乎是不现实的。
- 保留旧的接口,通过版本来实现API接口的变更。
通常情况下,我们会选择第三种方式来实现API接口的变更。
实现策略
URI
https://api-v1.example.com/places
https://api.example.com/v1/places
上述两种方式都是分别通过Path和Hostname来进行versioning。这种方式的好处直观,友好,易于理解,“复制&粘贴”更为友好;但是RESTful本身就不是“复制&粘贴”友好的。RESTafarian们根本不认同这是RESTful API,因为它破坏了HATEOAS,直接称它为TUK(The URL is King)。
请求体,Query参数
POST/placesHTTP/1.1 Host:api.example.com Content-Type:application/json { "version" : "1.0" 7 }
如果客户端的请求是JSON格式的,实现起来倒是不难。如果是Content-Type是image/png 或者是text/csv呢,这就不好处理。
或者把参数放到Query参数里:
POST/places?version=1.0HTTP/1.1
这种情况下,如果是POST请求呢,一些框架在POST请求的时候会直接忽略GET参数,虽然这有悖于HTTP协议,但是在POST请求带入GET参数,还是让人非常困惑的。
自定义请求头
GET/placesHTTP/1.1 Host:api.example.com BadApiVersion:1.0
看起来好像挺好的,但是这绝对代码中的坏味道。为了让缓存系统能够正确低返回果,我们Response必须是这个样子的:
HTTP/1.1200OK BadAPIVersion:1.1 Vary:BadAPIVersion
如果不指定Vary,像varnish缓存系统是不知道如何缓存这样的请求的。另外,抛开这点不说,要知道这个HTTP还必须通过查阅文档才能了解,这也挺恼人的。关于Vary头,请参考6。
Content Negotiation
这种方式是符合HATEOAS,也是相对最优雅的方式。Github API的Versioning就是通过这种方式实现的。Accept:application/vnd.github.user.v4+json Accept:application/vnd.github.user+json;version=4.0
这种方式,对HATEOAS和缓存都非常友好。唯一可能有点麻烦的就是实现, 主流的框架都没有处理自动根据请求的Content-Type处理的机制。
代码实现
对于URI的实现策略,最直接有效的实现方式就是通过多个代码库实现API的多个版本。部署时候把不同的版本部署到不同的server上。像这样:
- 1.0/master
- 1.0/develop
- 2.0/master
2.0/develop
有一个非常好的Git Flow可以参考。
对于Content Negotiation需要同一个代码库的实现,这个是重点。
以Rails为例。 Rails本身有一个很好的Gem叫做versionist实现了这个功能。
为了更好的理解其实现,我们可以自己实现以下。
首先是路由,给v1版本设置namespace
/config/routes.rb
接着是v1的实现
/app/controllers/api/v1/products_controller.rb
然后,v2版本需要变更这个API
###数据库变更###
/config/db/migrations/201205230000_change_products_released_on.rb
这样V2版本已经能正确返回的修改后的结果了。
###保留V1###
这里两个版本的ProductsController有很多重复代码,不太符合DRY原则。旧版本的代码的保留通常了为了兼容,总有那么一天旧版本的API废止了,那么重构就不值得了。如果确实觉得需要,可以把共通的行为提取到超类里面。